# coding: utf-8
#
import asyncio
import pytz
import urllib
import json
from threading import RLock
from retry import retry
from datetime import timedelta
from typing import Tuple
from logzero import logger
from web.views.base import (BaseRequestHandler, CorsMixin)
from apscheduler.events import EVENT_ALL, EVENT_JOB_ADDED, EVENT_JOB_EXECUTED, EVENT_JOB_REMOVED
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from tornado.httpclient import HTTPError, HTTPResponse
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.ioloop import IOLoop
from rethinkdb import r
from .. database import db
from .. database import time_now

class JobManager(object):
    '''
    负责管理任务的创建、分发、状态同步。
    '''
    def __init__(self) -> None:
        self._scheduler = self._setup_schedule()
        self._job_locker = self._create_lock()
        self._factory = None
        IOLoop.current().add_callback(self.update_job)

    def _setup_schedule(self):
        executors = {
            'default': ThreadPoolExecutor(20),
            'processpool': ProcessPoolExecutor(5)
        }
        scheduler = BackgroundScheduler(executors = executors)
        scheduler.start()
        scheduler.add_listener(self._scheduler_callback, EVENT_ALL)
        return scheduler

    async def update_job(self):
        '''
            如果设备第一次接入，创建相应job
            同步设备连接状态，更改jobs disabled和disabledReason
        '''
        reql = db.table_devices
        conn, feed = await reql.watch()
        with conn:
            while await feed.fetch_next():
                # if not self.__opened:
                #     break
                data = await feed.next()
                # TODO:补充iPhone手机逻辑。
                if data['new_val']['platform'] == 'apple':
                    return

                # initial the variables
                udid = data['new_val']["udid"]
                present = data['new_val']['present']
                app_name = data['new_val']['appName']
                task_type = data['new_val']['providerTaskType']
                now = time_now()

                #清理与当前provider执行的任务类型不一致的任务信息
                await db.table("jobs").filter(lambda v : v['udid'] == udid and v["jobType"] != task_type).delete()
                # create the job
                self._factory = JobBuilderFactory(udid=udid, app_name=app_name, job_type=task_type, scheduled_at=now, present=present)
                # try retrieving the job from data.
                existed_job = await db.table("jobs").get(self._factory.job_id).run()
                
                if(not existed_job is None):
                    # if device is online and job has been stored in db. then reschedule a job, and update the status to db.
                    if present:
                        await self._schedule_job(existed_job["jobId"], now + timedelta(minutes=1), existed_job["schdulePerior"])
                        logger.info("设备 [ %s ]在线，Job:[ %s ] 状态已经更改。[ %s ]", existed_job['udid'], existed_job["jobId"], existed_job)
                    else:
                        now = None
                        await self._remove_job(existed_job["jobId"], existed_job['udid'])
                        await self._restore_addtional_data_status(existed_job)
                        logger.info("设备 [ %s ]离线，Job:[ %s ] 状态已经更改。[ %s ]", existed_job['udid'], existed_job["jobId"], existed_job)
                        
                    # Only disabled or undisabled job. a disabled job cannot be scheduled or rescheduled.
                    existed_job.update({
                        # "scheduledAt":now,
                        "disabled":True if not present else False,
                        "disabledReason":"device offline" if not present else "",
                        "updatedAt":time_now()
                    })
                    await db.table("jobs").save(existed_job, existed_job["jobId"])
                    
                else:
                    # if device is online and job has been stored in db, then schedule a job 
                    # if device is offline and job has not been stored in db, don't need to do anything.
                    #当发现有新设备接入的时候，构建一个新的任务。
                    job = self._factory.create(task_type)
                    if present:
                        await self._schedule_job(self._factory.job_id, now + timedelta(minutes=1), job['schdulePerior'])
                        await db.table("jobs").save(job, self._factory.job_id)
                    logger.info("设备 [ %s ]在线，Job:[ %s ] 新建成功。[ %s ]", udid, self._factory.job_id, job)
    
    async def _restore_addtional_data_status(self, job):
        if job['jobType'] == 'adfe':
            items = await db.table('contacts') \
                .filter(lambda v: v["status"] == 'using' and v["udid"] == job['udid']) \
                    .order_by(r.asc('createAt')).run()
            for item in items:
                item['udid'] = ''
                item['provider'] = ''
                item['status'] = 'unused'
                item['updateAt'] = time_now()
                await db.table('contacts').save(item, item['id'])
                
    def _get_source_local(self, sources):
        source = None
        if not sources is None:
            for s in sorted(sources.values(), key=lambda v: v['priority'], reverse=True):
                source = s
                break
        return source

    def _execute(self, job_id):
        # For executing the awaitable method.
        asyncio.run(self._execute_async(job_id))

    async def _execute_async(self, job_id):
        '''
        Call the api of provider to distribute the job
        '''
        job = await db.table('jobs').get(job_id).run()
        logger.info("Job [ %s ] is executing...", job_id)
        # TODO:需要进一步调查
        # check if there are tasks running in provider
        if not job is None and \
            not job['disabled']:
            logger.info("Job [ %s ] is calling the api to distribute job...", job_id)

            # get the latest source of device
            source = await self._get_device_source(job["udid"])
            
             # call provider api to distribute the job.
             # the url pf provider get from devices source which is pripor   
            url = source['url'] + "/app/mobile/job?" + urllib.parse.urlencode(
                    dict(secret=source['secret']))
            try:
                logger.info("device [%s] was sending the command that running wechat operation throught provider [%s].", job['udid'], source['url'])
                # 不同类型的job可能需要依赖额外的数据才能完成任务的下发。
                if not await self._fill_addtional_data(job, source):
                    # TODO 这里需要确定，如果没有更多联系人将如何处理
                    return

                response = await self._distribute_job(url, job)
                if response.code == "200":
                    data = json.loads(response.body)
                    if data["success"]:
                        logger.info(" %s on device [%s] throught provider [%s].", data["description"], job['udid'], source['url'])
                    else:
                        logger.info(" Job wasn't accepted on device [%s] throught provider [%s].", data["description"], job['udid'], source['url'])
            except HTTPError as e:
                await self._remove_job(job_id=job['jobId'], udid=job['udid'])
                logger.error("device [%s] was sending the command of wechat operation process error: %s", job['udid'], e)

    # @retry((HTTPError), delay=1, tries=3, backoff=2)
    async def _distribute_job(self, url, job) -> HTTPResponse:
        http_client = AsyncHTTPClient()
        headers = {'Content-Type': 'application/json'}
        json_data = await self.job_to_json(job)
        request = HTTPRequest(url, method="POST", body=json_data, headers=headers)
        response = await http_client.fetch(request)
        return response
        
    async def _fill_addtional_data(self, job, source) -> bool:
        '''
        retrieve the addtional data for a special type job. 
        '''
        # if job.jobType == 'adfg'
        # don't need do anything here.
        if job['jobType'] == 'adfe':
            job['contacts'] = await self._get_contacts(job, source)
            if len(job['contacts']) <= 0:
                logger.warning('任务 [ %s ], 类型 [ %s ] 没有可提供的联系人。', job["jobId"], job['jobType'])
                return False
        return True
    
    async def _get_device_source(self, udid:str) -> dict:
        '''
        Retrieve the source of device.
        '''
        # The source may empty when the provider is offline.
        device = await db.table("devices").get(udid).pluck("udid", "sources").run()
        sources = device.get('sources', {}).values()
        for s in sorted(sources, key=lambda v: v['priority'], reverse=True):
            return s

    async def job_to_json(self, job:dict) -> str:
        """
            转换job数据为json
        """
        data = self._factory.create_job_req(job)
        return json.dumps(data)

    def _scheduler_callback(self, event):
        """稍后实现"""
        if(event.code == EVENT_JOB_ADDED):
            logger.info("Job [ %s ] is added...", event.job_id)
        if(event.code == EVENT_JOB_REMOVED):
            logger.info("Job [ %s ] is removed...", event.job_id)
        if(event.code == EVENT_JOB_EXECUTED):
            logger.info("Job [ %s ] is executed...", event.job_id)
    
    async def _schedule_job(self, job_id, start_date, schedule_perior) -> Tuple:
        
        if  self._scheduler.get_job(job_id) is None:
            # timezone="UTC+08:00"
            #计划一个任务，每天在start_date执行，到24小时后结束。
            # temp_id = job_id + "-" + start_date.strftime("%Y-%m-%d %H:%M:%S")
            self._scheduler.add_job(self._execute, "interval", hours=schedule_perior, \
                start_date=start_date, timezone=pytz.FixedOffset(8), args={job_id}, id=job_id, replace_existing = False)
            logger.info("Job:[%s] 已经规划，于%s执行", job_id, start_date)
            
    async def _remove_job(self, job_id, udid):
        if not self._scheduler.get_job(job_id) is None:
            self._scheduler.remove_job(job_id)
            logger.info("设备：[%s], Job:[ %s ] 已经被移除。", udid, job_id)

    async def _get_contacts(self, job, source) -> list:
        # 同时只有一个线程可以读取和更改rethinkdb中contacts表的数据。
        lst = []
        task_load = job['taskLoad']
        with self._job_locker:
            items = await db.table('contacts') \
                .filter(lambda v: v["status"] == 'unused') \
                    .order_by(r.asc('createAt')) \
                        .limit(task_load).run()
            for item in items:
                lst.append(item.copy())
                item['udid'] = job['udid']
                item['provider'] = source['id']
                item['status'] = 'using'
                item['updateAt'] = time_now()
                await db.table('contacts').save(item, item['id'])
        return lst
            
            

    def _create_lock(self):
        '''
        Creates a reentrant lock object.
        '''
        return RLock()

job_manager = JobManager()

class JobStatusChangeHandler(CorsMixin, BaseRequestHandler):
    '''
    REST api for handling job status sync.
    '''
    async def post(self, *argus, **kargus):
        '''
            sync the status of job.
            request.body:
            {
                'jobId':str,
                'taskType':xxx,
                status:{
                    'completedTaskLoad':int
                    'lastGroupIndex':int
                    'lastMemberIndex':int
                }
                or 
                status:{
                    'completedTaskLoad':,
                    'phoneNumber': ,
                    'result':'completed' or 'recycled'
                }
            }
        '''
        data = json.loads(self.request.body)
        job = await db.table('jobs').get(data['jobId']).run()
        if not job is None:
            if data['taskType'] == job['jobType'] == 'adfg':
                job['completedTaskLoad'] = data['status']['completedTaskLoad']
                job['optData']['lastGroupIndex'] = data['status']['lastGroupIndex']
                job['optData']['lastMemberIndex'] = data['status']['lastMemberIndex']
                await db.table('jobs').save(job, job['jobId'])

            if data['taskType'] == job['jobType'] == 'adfe':
                phone = data['status']['phoneNumber']
                result = data['status']['result']
                record = await db.table('contacts').get(phone).run()
                if not record is None :
                    await db.table('contacts').get(record['id']).update({'status':result, 'updateAt':time_now()})
        self.write_json({"success":True, "description":"The job status has been updated"})

class RestorePhoneStatusHandler(CorsMixin, BaseRequestHandler):
    '''
    REST api for handling job status sync.
    '''
    async def post(self, *argus, **kargus):
        '''
            sync the status of job.
            request.body:
            {
                phones:['xxx', 'xxx', 'xxx']
            }
        '''
        data = json.loads(self.request.body)
        phones = None
        if (data is None) or (not 'phones' in data.keys()) or len(data['phones']) <= 0:
            self.write_json({"success":False, "description":"The phones wasn't be providered in request."})
            return

        phones = data['phones']   
        for phone in phones:
            record = await db.table('contacts').get(phone).run()
            if not record is None and record['status'] == 'using':
                data = {
                    'udid':'',
                    'provider':'',
                    'status':'unused', 
                    'updateAt':time_now()
                }
                await db.table('contacts').get(record['id']).update(data)
            else:
                logger.warn("手机号 %s 无需更新状态。", phone)

        self.write_json({"success":True, "description":"The job status has been updated"})

class JobBuilderFactory(object):
    '''
    Factory is to create the job according to the jobType and job distr request.
    '''
    def __init__(self, udid, app_name, job_type, scheduled_at, present: bool) -> None:
        self._udid = udid
        self._app_name = app_name
        self._job_type = job_type
        self._schedule_at = scheduled_at
        self._present = present
    
    def create(self, job_type) -> dict:
        '''
        根据job_type构建不同的任务字典数据。
        '''
        job = None
        if job_type == "adfg":
            logger.info("正在构建 %s 类型的job。", job_type)
            job = self._create_adfg()
        if job_type == "adfe":
            logger.info("正在构建 %s 类型的job。", job_type)
            job = self._create_adfe()

        if job is None:
            logger.warn("无法构建 %s 类型的job。", job_type)
        
        return job

    def create_job_req(self, job) -> dict:
        '''
        根据job构建不同的任务下发requst。
        '''
        data = None

        if job['jobType'] == 'adfg':
            data = self._create_req_adfg(job)
        if job['jobType'] == 'adfe':
            data = self._create_req_adfe(job)
        
        if data is None:
            logger.warn("构建任务下发request，无法识别 [ %s ] 任务类型", job['jobType'])

        return data

    @property
    def job_id(self):
        '''
        job_id构成形式为：‘设备udid-app_name-job_type’
        '''
        return self._udid + "-" + self._app_name + "-" + self._job_type

    def _create_adfg(self):
        ''''''
        job = {
            "jobId":self.job_id,
            "udid": self._udid,
            "appName": self._app_name,
            "jobType": self._job_type,
            # "scheduledAt":self._schedule_at,
            "taskLoad":40,
            "completedTaskLoad":0,
            "optData":{
                "lastGroupIndex":0,
                "lastMemberIndex":0
            },
            "schdulePerior":24, # 单位为小时
            "status":"scheduled",
            "disabled":True if not self._present else False,
            "disabledReason":"",
            "updatedAt":time_now()
        }
        return job

    def _create_adfe(self):
        job = {
            "jobId":self.job_id,
            "udid": self._udid,
            "appName": self._app_name,
            "jobType": self._job_type,
            # "scheduledAt":self._schedule_at,
            "taskLoad":70,
            "completedTaskLoad":0,
            "schdulePerior":24, # 单位为小时
            "status":"scheduled",
            "disabled":True if not self._present else False,
            "disabledReason":"",
            "updatedAt":time_now()
        }
        return job

    def _create_req_adfg(self, job):
        data = {
            "job_id":job['jobId'],
            "udid":job['udid'],
            "app_name":job['appName'],
            "job_type":job['jobType'],
            "task_load":job['taskLoad'],
            # 这里无法依据scheduleAt来确定要执行的时间。
            "plan_datetime":time_now().strftime('%Y-%m-%d %H:%M:%S'),
            "operator_data":{
                "current_group_index":job['optData']['lastGroupIndex'],
                "current_member_index":job['optData']['lastMemberIndex']
            },
            "task_generating_pattern":job['schdulePerior']
        }
        return data
    
    def _create_req_adfe(self, job):
        task_load = job['taskLoad'] if job['taskLoad'] <= len(job['contacts']) \
                                    else len(job['contacts'])
        contacts = [] 
        for item in job['contacts']:
            contacts.append(item['id'])
            
        data = {
            "job_id":job['jobId'],
            "udid":job['udid'],
            "app_name":job['appName'],
            "job_type":job['jobType'],
            "task_load":task_load,
            # 这里无法依据scheduleAt来确定要执行的时间。
            "plan_datetime":time_now().strftime('%Y-%m-%d %H:%M:%S'),
            "operator_data":{
                "cellphones": contacts,
                "current_phone_index":0,
                # city and indstry暂时没有使用到。
                "city":None,
                "industry":None,
            },

            "task_generating_pattern":job['schdulePerior']
        }
        return data